﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using System.Drawing.Design;
using Windows.Win32.UI.Controls.Dialogs;
using static Windows.Win32.UI.Controls.Dialogs.OPEN_FILENAME_FLAGS;
using static Windows.Win32.UI.Shell.FILEOPENDIALOGOPTIONS;

namespace System.Windows.Forms;

/// <summary>
///  Displays a dialog window from which the user can select a file.
/// </summary>
[DefaultEvent(nameof(FileOk))]
[DefaultProperty(nameof(FileName))]
public abstract partial class FileDialog : CommonDialog
{
    private const int FileBufferSize = 8192;
    private static readonly char[] s_wildcards = ['*', '?'];

    protected static readonly object EventFileOk = new();

    private protected OPEN_FILENAME_FLAGS _fileNameFlags;
    private protected FILEOPENDIALOGOPTIONS _dialogOptions;

    private string? _title;
    private string? _initialDirectory;
    private string? _defaultExtension;
    private string[]? _fileNames;
    private string? _filter;
    private bool _ignoreSecondFileOkNotification;
    private int _okNotificationCount;
    private char[]? _charBuffer;
    private HWND _dialogHWnd;

    /// <summary>
    ///  In an inherited class, initializes a new instance of the <see cref="FileDialog"/> class.
    /// </summary>
    internal FileDialog()
    {
        Reset();
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box automatically adds an
    ///  extension to a file name if the user omits the extension.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.FDaddExtensionDescr))]
    public bool AddExtension { get; set; }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box adds the file being opened
    ///  or saved to the recent list.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.FileDialogAddToRecentDescr))]
    public bool AddToRecent
    {
        get => !GetOption(OFN_DONTADDTORECENT);
        set => SetOption(OFN_DONTADDTORECENT, !value);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box displays a warning
    ///  if the user specifies a file name that does not exist.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.FDcheckFileExistsDescr))]
    public virtual bool CheckFileExists
    {
        get => GetOption(OFN_FILEMUSTEXIST);
        set => SetOption(OFN_FILEMUSTEXIST, value);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box displays a warning if
    ///  the user specifies a path that does not exist.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.FDcheckPathExistsDescr))]
    public bool CheckPathExists
    {
        get => GetOption(OFN_PATHMUSTEXIST);
        set => SetOption(OFN_PATHMUSTEXIST, value);
    }

    /// <summary>
    ///  <para>
    ///   Gets or sets the GUID to associate with this dialog state. Typically, state such
    ///   as the last visited folder and the position and size of the dialog is persisted
    ///   based on the name of the executable file. By specifying a GUID, an application can
    ///   have different persisted states for different versions of the dialog within the
    ///   same application (for example, an import dialog and an open dialog).
    ///  </para>
    ///  <para>
    ///   This functionality is not available if an application is not using visual styles
    ///   or if <see cref="AutoUpgradeEnabled"/> is set to <see langword="false"/>.
    ///  </para>
    /// </summary>
    [Localizable(false)]
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public Guid? ClientGuid { get; set; }

    /// <summary>
    ///  Gets or sets the default file extension.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue("")]
    [SRDescription(nameof(SR.FDdefaultExtDescr))]
    [AllowNull]
    public string DefaultExt
    {
        get => _defaultExtension ?? string.Empty;
        set
        {
            string? defaultExt = value;
            if (defaultExt is not null)
            {
                if (defaultExt.StartsWith('.'))
                {
                    defaultExt = defaultExt[1..];
                }
                else if (defaultExt.Length == 0)
                {
                    defaultExt = null;
                }
            }

            _defaultExtension = defaultExt;
        }
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box returns the location
    ///  of the file referenced by the shortcut or whether it returns the location
    ///  of the shortcut (.lnk).
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.FDdereferenceLinksDescr))]
    public bool DereferenceLinks
    {
        get => !GetOption(OFN_NODEREFERENCELINKS);
        set => SetOption(OFN_NODEREFERENCELINKS, !value);
    }

    private protected string DialogCaption => PInvoke.GetWindowText(_dialogHWnd);

    /// <summary>
    ///  Gets or sets a string containing the file name selected in the file dialog box.
    /// </summary>
    [SRCategory(nameof(SR.CatData))]
    [DefaultValue("")]
    [SRDescription(nameof(SR.FDfileNameDescr))]
    [AllowNull]
    public string FileName
    {
        get => _fileNames is { } names && names.Length > 0 ? names[0] : string.Empty;
        set => _fileNames = value is not null ? [value] : null;
    }

    /// <summary>
    ///  Gets the file names of all selected files in the dialog box.
    /// </summary>
    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    [SRDescription(nameof(SR.FDFileNamesDescr))]
    [AllowNull]
    public string[] FileNames
    {
        get => _fileNames is not null ? (string[])_fileNames.Clone() : [];
    }

    /// <summary>
    ///  Gets or sets the current file name filter string, which determines the choices
    ///  that appear in the "Save as file type" or "Files of type" box in the dialog box.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue("")]
    [Localizable(true)]
    [SRDescription(nameof(SR.FDfilterDescr))]
    [AllowNull]
    public string Filter
    {
        get => _filter ?? string.Empty;
        set
        {
            if (value == _filter)
            {
                return;
            }

            if (!string.IsNullOrEmpty(value))
            {
                int pipeCount = value.AsSpan().Count('|');
                if (pipeCount % 2 == 0)
                {
                    throw new ArgumentException(SR.FileDialogInvalidFilter, nameof(value));
                }
            }
            else
            {
                value = null!;
            }

            _filter = value;
        }
    }

    /// <summary>
    ///  Extracts the file extensions specified by the current file filter into an
    ///  array of strings.  None of the extensions contain .'s, and the  default
    ///  extension is first.
    /// </summary>
    private string[] FilterExtensions
    {
        get
        {
            string? filter = _filter;
            List<string> extensions = [];

            // First extension is the default one. It's not expected that DefaultExt
            // is not in the filters list, but this is legal.
            if (_defaultExtension is not null)
            {
                extensions.Add(_defaultExtension);
            }

            if (filter is not null)
            {
                string[] tokens = filter.Split('|');

                if ((FilterIndex * 2) - 1 >= tokens.Length)
                {
                    throw new InvalidOperationException(SR.FileDialogInvalidFilterIndex);
                }

                if (FilterIndex > 0)
                {
                    string[] exts = tokens[(FilterIndex * 2) - 1].Split(';');
                    foreach (string ext in exts)
                    {
                        int i = SupportMultiDottedExtensions ? ext.IndexOf('.') : ext.LastIndexOf('.');
                        if (i >= 0)
                        {
                            extensions.Add(ext[(i + 1)..]);
                        }
                    }
                }
            }

            return [.. extensions];
        }
    }

    /// <summary>
    ///  Gets or sets the index of the filter currently selected in the file dialog box.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(1)]
    [SRDescription(nameof(SR.FDfilterIndexDescr))]
    public int FilterIndex { get; set; }

    /// <summary>
    ///  Gets or sets the initial directory displayed by the file dialog box.
    /// </summary>
    [SRCategory(nameof(SR.CatData))]
    [DefaultValue("")]
    [Editor("System.Windows.Forms.Design.InitialDirectoryEditor, System.Windows.Forms.Design, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", typeof(UITypeEditor))]
    [SRDescription(nameof(SR.FDinitialDirDescr))]
    [AllowNull]
    public string InitialDirectory
    {
        get => _initialDirectory ?? string.Empty;
        set => _initialDirectory = value;
    }

    /// <summary>
    ///  Gets the Win32 instance handle for the application.
    /// </summary>
    protected virtual nint Instance => PInvoke.GetModuleHandle((PCWSTR)null);

    /// <summary>
    ///  Gets the Win32 common Open File Dialog OFN_* and FOS_* option flags.
    /// </summary>
    protected int Options =>
        (int)(_fileNameFlags & (OFN_READONLY | OFN_HIDEREADONLY | OFN_NOCHANGEDIR | OFN_SHOWHELP | OFN_NOVALIDATE
          | OFN_ALLOWMULTISELECT | OFN_PATHMUSTEXIST | OFN_NODEREFERENCELINKS | OFN_DONTADDTORECENT
          | OFN_NOREADONLYRETURN | OFN_NOTESTFILECREATE | OFN_FORCESHOWHIDDEN))
          |
        (int)(_dialogOptions & (FOS_OKBUTTONNEEDSINTERACTION | FOS_HIDEPINNEDPLACES | FOS_DEFAULTNOMINIMODE | FOS_FORCEPREVIEWPANEON));

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box restores the current
    ///  directory before closing.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.FDrestoreDirectoryDescr))]
    public bool RestoreDirectory
    {
        get => GetOption(OFN_NOCHANGEDIR);
        set => SetOption(OFN_NOCHANGEDIR, value);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether whether the Help button is displayed
    ///  in the file dialog.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.FDshowHelpDescr))]
    public bool ShowHelp
    {
        get => GetOption(OFN_SHOWHELP);
        set => SetOption(OFN_SHOWHELP, value);
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box displays hidden and system files.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.FileDialogShowHiddenFilesDescr))]
    public bool ShowHiddenFiles
    {
        get => GetOption(OFN_FORCESHOWHIDDEN);
        set => SetOption(OFN_FORCESHOWHIDDEN, value);
    }

    /// <summary>
    ///  Gets or sets whether def or abc.def is the extension of the file filename.abc.def
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(false)]
    [SRDescription(nameof(SR.FDsupportMultiDottedExtensionsDescr))]
    public bool SupportMultiDottedExtensions { get; set; }

    /// <summary>
    ///  Gets or sets the file dialog box title.
    /// </summary>
    [SRCategory(nameof(SR.CatAppearance))]
    [DefaultValue("")]
    [Localizable(true)]
    [SRDescription(nameof(SR.FDtitleDescr))]
    [AllowNull]
    public string Title
    {
        get => _title ?? string.Empty;
        set => _title = value;
    }

    /// <summary>
    ///  Gets or sets a value indicating whether the dialog box accepts only valid Win32 file names.
    /// </summary>
    [SRCategory(nameof(SR.CatBehavior))]
    [DefaultValue(true)]
    [SRDescription(nameof(SR.FDvalidateNamesDescr))]
    public bool ValidateNames
    {
        get => !GetOption(OFN_NOVALIDATE);
        set => SetOption(OFN_NOVALIDATE, !value);
    }

    /// <summary>
    ///  Occurs when the user clicks on the Open or Save button on a file dialog box.
    /// </summary>
    [SRDescription(nameof(SR.FDfileOkDescr))]
    public event CancelEventHandler FileOk
    {
        add => Events.AddHandler(EventFileOk, value);
        remove => Events.RemoveHandler(EventFileOk, value);
    }

    /// <summary>
    ///  Processes the CDN_FILEOK notification.
    /// </summary>
    private unsafe bool DoFileOk(OPENFILENAME* lpOFN)
    {
        OPEN_FILENAME_FLAGS saveOptions = _fileNameFlags;
        int saveFilterIndex = FilterIndex;
        string[]? saveFileNames = _fileNames;
        bool ok = false;
        try
        {
            _fileNameFlags = _fileNameFlags & ~OFN_READONLY | lpOFN->Flags & OFN_READONLY;
            FilterIndex = (int)lpOFN->nFilterIndex;

            Thread.MemoryBarrier();

            _fileNames = _fileNameFlags.HasFlag(OFN_ALLOWMULTISELECT)
                ? GetMultiselectFiles(new((char*)lpOFN->lpstrFile, (int)lpOFN->nMaxFile))
                : [lpOFN->lpstrFile.ToString()];

            if (!ProcessFileNames(_fileNames))
            {
                return ok;
            }

            CancelEventArgs ceevent = new();
            if (NativeWindow.WndProcShouldBeDebuggable)
            {
                OnFileOk(ceevent);
                ok = !ceevent.Cancel;
            }
            else
            {
                try
                {
                    OnFileOk(ceevent);
                    ok = !ceevent.Cancel;
                }
                catch (Exception e)
                {
                    Application.OnThreadException(e);
                }
            }

            return ok;
        }
        finally
        {
            if (!ok)
            {
                Thread.MemoryBarrier();
                _fileNames = saveFileNames;

                _fileNameFlags = saveOptions;
                FilterIndex = saveFilterIndex;
            }
        }
    }

    private protected static bool FileExists(string? fileName)
    {
        try
        {
            return File.Exists(fileName);
        }
        catch (PathTooLongException)
        {
            return false;
        }
    }

    /// <summary>
    ///  Extracts the filename(s) returned by the file dialog.
    /// </summary>
    private static string[] GetMultiselectFiles(ReadOnlySpan<char> fileBuffer)
    {
        var directory = fileBuffer.SliceAtFirstNull();
        var fileNames = fileBuffer[(directory.Length + 1)..];

        // When a single file is returned, the directory is not null delimited.
        // So we check here to see if the filename starts with a null.
        if (fileNames.Length == 0 || fileNames[0] == '\0')
        {
            return [directory.ToString()];
        }

        List<string> names = [];
        var fileName = fileNames.SliceAtFirstNull();
        while (fileName.Length > 0)
        {
            names.Add(Path.IsPathFullyQualified(fileName)
                ? fileName.ToString()
                : Path.Join(directory, fileName));

            fileNames = fileNames[(fileName.Length + 1)..];
            fileName = fileNames.SliceAtFirstNull();
        }

        return [.. names];
    }

    /// <summary>
    ///  Returns the state of the given option flag.
    /// </summary>
    private protected bool GetOption(OPEN_FILENAME_FLAGS option) => (_fileNameFlags & option) != 0;

    /// <summary>
    ///  Defines the common dialog box hook procedure that is overridden to add
    ///  specific functionality to the file dialog box.
    /// </summary>
    protected override unsafe IntPtr HookProc(IntPtr hWnd, int msg, IntPtr wparam, IntPtr lparam)
    {
        if (msg != (int)PInvoke.WM_NOTIFY)
        {
            return IntPtr.Zero;
        }

        _dialogHWnd = PInvoke.GetParent((HWND)hWnd);
        try
        {
            OFNOTIFY* notify = (OFNOTIFY*)lparam;

            switch (notify->hdr.code)
            {
                case PInvoke.CDN_INITDONE:
                    MoveToScreenCenter(_dialogHWnd);
                    break;
                case PInvoke.CDN_SELCHANGE:
                    // Get the buffer size required to store the selected file names.
                    int sizeNeeded = (int)PInvoke.SendMessage(_dialogHWnd, PInvoke.CDM_GETSPEC);
                    if (sizeNeeded > notify->lpOFN->nMaxFile)
                    {
                        // A bigger buffer is required.
                        int newBufferSize = sizeNeeded + (FileBufferSize / 4);

                        // Allocate new buffer
                        _charBuffer = GC.AllocateArray<char>(newBufferSize, pinned: true);

                        fixed (char* buffer = _charBuffer)
                        {
                            // Substitute buffer
                            notify->lpOFN->lpstrFile = buffer;
                            notify->lpOFN->nMaxFile = (uint)newBufferSize;
                        }
                    }

                    _ignoreSecondFileOkNotification = false;
                    break;
                case PInvoke.CDN_SHAREVIOLATION:
                    // When the selected file is locked for writing,
                    // we get this notification followed by *two* CDN_FILEOK notifications.
                    _ignoreSecondFileOkNotification = true;  // We want to ignore the second CDN_FILEOK
                    _okNotificationCount = 0;                // to avoid a second prompt by PromptFileOverwrite.
                    break;
                case PInvoke.CDN_FILEOK:
                    if (_ignoreSecondFileOkNotification)
                    {
                        // We got a CDN_SHAREVIOLATION notification and want to ignore the second CDN_FILEOK notification
                        if (_okNotificationCount == 0)
                        {
                            // This one is the first and is all right.
                            _okNotificationCount = 1;
                        }
                        else
                        {
                            // This is the second CDN_FILEOK, so we want to ignore it.
                            _ignoreSecondFileOkNotification = false;
                            PInvoke.SetWindowLong((HWND)hWnd, 0, -1);
                            return -1;
                        }
                    }

                    if (!DoFileOk(notify->lpOFN))
                    {
                        PInvoke.SetWindowLong((HWND)hWnd, 0, -1);
                        return -1;
                    }

                    break;
            }
        }
        catch
        {
            if (!_dialogHWnd.IsNull)
            {
                PInvoke.EndDialog(_dialogHWnd, 0);
            }

            throw;
        }

        return IntPtr.Zero;
    }

    /// <summary>
    ///  Converts the given filter string to the format required in an OPENFILENAME structure.
    /// </summary>
    private static string? MakeFilterString(string? s, bool dereferenceLinks)
    {
        if (string.IsNullOrEmpty(s))
        {
            if (dereferenceLinks)
            {
                s = " |*.*";
            }
            else if (s is null)
            {
                return null;
            }
        }

        return string.Create(s.Length + 2, s, static (span, s) =>
        {
            s.AsSpan().Replace(span, '|', '\0');
            span[^1] = '\0';
        });
    }

    /// <summary>
    ///  Raises the <see cref="FileOk"/> event.
    /// </summary>
    protected void OnFileOk(CancelEventArgs e)
    {
        CancelEventHandler? handler = (CancelEventHandler?)Events[EventFileOk];
        handler?.Invoke(this, e);
    }

    /// <summary>
    ///  Processes the filenames entered in the dialog according to the settings of the <see cref="AddExtension"/>,
    ///  <see cref="CheckFileExists"/>, and <see cref="ValidateNames"/> properties.
    /// </summary>
    private bool ProcessFileNames(string[] fileNames)
    {
        if (!ValidateNames)
        {
            return true;
        }

        string[] extensions = FilterExtensions;
        for (int i = 0; i < fileNames.Length; i++)
        {
            string fileName = fileNames[i];
            if (AddExtension && !Path.HasExtension(fileName))
            {
                bool fileMustExist = CheckFileExists;

                for (int j = 0; j < extensions.Length; j++)
                {
                    var currentExtension = Path.GetExtension(fileName.AsSpan());

                    Debug.Assert(!extensions[j].StartsWith('.'),
                        "FileDialog.FilterExtensions should not return things starting with '.'");
                    Debug.Assert(currentExtension.Length == 0 || currentExtension[0] == '.',
                        "File.GetExtension should return something that starts with '.'");

                    // We don't want to append the extension if it contains wild cards
                    string s = extensions[j].IndexOfAny(s_wildcards) == -1
                        ? $"{fileName[..^currentExtension.Length]}.{extensions[j]}"
                        : fileName[..^currentExtension.Length];

                    if (!fileMustExist || FileExists(s))
                    {
                        fileName = s;
                        break;
                    }
                }

                fileNames[i] = fileName;
            }

            if (!PromptUserIfAppropriate(fileName))
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    ///  Prompts the user with a <see cref="MessageBox"/> with the given parameters. It also ensures that the
    ///  focus is set back on the window that had the focus to begin with (before we displayed the MessageBox).
    /// </summary>
    private protected static bool MessageBoxWithFocusRestore(
        string message,
        string caption,
        MessageBoxButtons buttons,
        MessageBoxIcon icon)
    {
        HWND focusHandle = PInvoke.GetFocus();
        try
        {
            return RTLAwareMessageBox.Show(null, message, caption, buttons, icon, MessageBoxDefaultButton.Button1, 0)
                == DialogResult.Yes;
        }
        finally
        {
            PInvoke.SetFocus(focusHandle);
        }
    }

    /// <summary>
    ///  If it's necessary to throw up a "This file exists, are you sure?" kind of MessageBox,
    ///  here's where we do it. Return value is whether or not the user hit "okay".
    /// </summary>
    private protected virtual bool PromptUserIfAppropriate(string fileName)
    {
        if (_fileNameFlags.HasFlag(OFN_FILEMUSTEXIST) && !FileExists(fileName))
        {
            MessageBoxWithFocusRestore(
                string.Format(SR.FileDialogFileNotFound, fileName),
                DialogCaption,
                MessageBoxButtons.OK,
                MessageBoxIcon.Warning);
            return false;
        }

        return true;
    }

    /// <summary>
    ///  Resets all properties to their default values.
    /// </summary>
    public override void Reset()
    {
        _fileNameFlags = OFN_HIDEREADONLY | OFN_PATHMUSTEXIST;
        _dialogOptions = default;
        AddExtension = true;
        _title = null;
        _initialDirectory = null;
        _defaultExtension = null;
        _fileNames = null;
        _filter = null;
        FilterIndex = 1;
        SupportMultiDottedExtensions = false;
        _customPlaces.Clear();
        ClientGuid = null;
    }

    /// <summary>
    ///  Implements running of a file dialog.
    /// </summary>
#pragma warning disable CA1725 // Parameter names should match base declaration - shipped and documented with this casing
    protected override bool RunDialog(IntPtr hWndOwner)
#pragma warning restore CA1725
    {
        if (Control.CheckForIllegalCrossThreadCalls && Application.OleRequired() != ApartmentState.STA)
        {
            throw new ThreadStateException(string.Format(SR.DebuggingExceptionOnly, SR.ThreadMustBeSTA));
        }

        // If running the Vista dialog fails (e.g. on Server Core), we fall back to the legacy dialog.
        if (UseVistaDialogInternal && TryRunDialogVista((HWND)hWndOwner, out bool returnValue))
        {
            return returnValue;
        }

        return RunDialogOld((HWND)hWndOwner);
    }

    private unsafe bool RunDialogOld(HWND owner)
    {
        _charBuffer = GC.AllocateArray<char>(FileBufferSize, pinned: true);
        FileName.CopyTo(_charBuffer);

        string? filterString = MakeFilterString(_filter, DereferenceLinks);

        fixed (char* buffer = _charBuffer)
        fixed (char* filter = filterString)
        fixed (char* initialDirectory = _initialDirectory)
        fixed (char* title = _title)
        fixed (char* extension = _defaultExtension)
        {
            OPENFILENAME ofn = new()
            {
                lStructSize = (uint)sizeof(OPENFILENAME),
                hwndOwner = owner,
                hInstance = (HINSTANCE)Instance,
                lpstrFilter = filter,
                nFilterIndex = (uint)FilterIndex,
                lpstrFile = buffer,
                nMaxFile = (uint)_charBuffer.Length,
                lpstrInitialDir = initialDirectory,
                lpstrTitle = title,
                Flags = (OPEN_FILENAME_FLAGS)Options | OFN_EXPLORER | OFN_ENABLEHOOK | OFN_ENABLESIZING,
                FlagsEx = OPEN_FILENAME_FLAGS_EX.OFN_EX_NONE,
                lpstrDefExt = AddExtension ? extension : null,
                lpfnHook = HookProcFunctionPointer
            };

            try
            {
                return RunFileDialog(&ofn);
            }
            finally
            {
                _charBuffer = null;
            }
        }
    }

    /// <summary>
    ///  Implements the actual call to GetOpenFileName or GetSaveFileName.
    /// </summary>
    private protected abstract unsafe bool RunFileDialog(OPENFILENAME* ofn);

    /// <summary>
    ///  Sets the given option to the given boolean value.
    /// </summary>
    private protected void SetOption(OPEN_FILENAME_FLAGS option, bool value)
    {
        if (value)
        {
            _fileNameFlags |= option;
        }
        else
        {
            _fileNameFlags &= ~option;
        }
    }

    public override string ToString() => $"{base.ToString()}: Title: {Title}, FileName: {FileName}";
}
